package com.sz.app.oss.api;

import com.sz.app.oss.constants.MessagesCodes;
import com.sz.app.oss.constants.ModuleNames;
import com.sz.app.oss.exception.ErrorCodes;
import com.sz.biz.app.dto.LoginDataDto;
import com.sz.biz.app.dto.TokenDto;
import com.sz.biz.app.web.config.SystemSettings;
import com.sz.biz.app.web.security.Des;
import com.sz.biz.app.web.security.OnlineSessionManager;
import com.sz.common.base.constants.UserActions;
import com.sz.common.base.dto.ResultDto;
import com.sz.common.base.i18n.I18nMessages;
import com.sz.common.base.logger.Logger;
import com.sz.common.core.service.Principal;
import com.sz.common.core.service.PrincipalLogUtils;
import com.sz.common.core.service.PrincipalUtils;
import com.sz.common.core.system.entity.UserLoginInfo;
import com.sz.common.core.system.service.UserLoginInfoService;
import com.sz.app.oss.web.security.SecurityKeys;
import io.swagger.annotations.Api;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * Function: SecurityController <br>
 * Author: xutao <br>
 * Date: 2017-01-05 10:53:00
 */

@RestController
@RequestMapping("/api/v1/auth")
@Api(description = " ", tags = "000-0、安全登录")
public class SecurityController {

    public static final String UNKNOWN = "unknown";
    @Autowired
    private SecurityKeys securityKeys;

    @Autowired
    private OnlineSessionManager onlineSessionManager;

    @Autowired
    private UserLoginInfoService userLoginInfoService;

    @Autowired
    private SystemSettings systemSettings;

    @Autowired
    protected I18nMessages messageSource;

    protected Logger logger = Logger.getLogger(this.getClass());

    @RequestMapping(value = "/whoami", method = RequestMethod.GET)
    public ResultDto whoami() {
        Serializable id = SecurityUtils.getSubject().getSession().getId();
        ResultDto result = new ResultDto();
        result.setData(id);
        return result;
    }

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public ResultDto login(@RequestBody LoginDataDto loginDataDto, HttpServletRequest request) throws Exception {
        //check v code first
        if (!checkVCode(loginDataDto.getValidateCode())) {
            return ResultDto.createResult(ErrorCodes.ERROR_LOGIN_VALIDATE_CODE_ERR);
        }
        String username = loginDataDto.getUserName();
        String password = decPassword(loginDataDto.getPassWord());

        //获取用户登录时间
        UserLoginInfo userLoginInfo = new UserLoginInfo();
        userLoginInfo.setUserName(username);
        userLoginInfo.setUserCode(username);
        userLoginInfo.setLoginTime(new Date());
        //获取用户登录IP
        String cliIp = getIpAddr(request);
        userLoginInfo.setLoginIp(cliIp);
        logger.debug("-----获取用户的IP地址为：" + cliIp + "--------------");

        //记录当前用户到session，方便后续使用
        SecurityUtils.getSubject().getSession().setAttribute("username", username);

        //获取当前的Subject
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        //登录过程
        try {
            //在调用了login方法后,SecurityManager会收到AuthenticationToken,并将其发送给已配置的Realm执行必须的认证检查
            //每个Realm都能在必要时对提交的AuthenticationTokens作出反应
            //所以这一步在调用login(token)方法时,它会走到MyRealm.doGetAuthenticationInfo()方法中,具体验证方式详见此方法
            logger.info("对用户[" + username + "]进行登录验证..验证开始");
            subject.login(token);
            logger.info("对用户[" + username + "]进行登录验证..验证通过");
            userLoginInfo.setErrCode(0);
            userLoginInfo.setLoginStatus(true);
            userLoginInfoService.updateUserLoginInfo(userLoginInfo);
        } catch (UnknownAccountException uae) {
            logger.warn("对用户[" + username + "]进行登录验证..验证未通过,未知账户");
            return ResultDto.createResult(ErrorCodes.ERROR_LOGIN_UNKNOWN_ACCOUNT);
        } catch (IncorrectCredentialsException ice) {
            logger.warn("对用户[" + username + "]进行登录验证..验证未通过,错误的凭证");
            userLoginInfo.setErrCode(ErrorCodes.ERROR_LOGIN_WRONG_PASSWORD.getCode());
            userLoginInfo.setLoginStatus(false);
            userLoginInfoService.updateUserLoginInfo(userLoginInfo);

            //统计密码输入错误后剩余的输入机会次数给客户端
            int leftInputCount = (int) SecurityUtils.getSubject().getSession().getAttribute("leftInputCount");

            ResultDto resultDto = new ResultDto();
            Map<String, Object> dataMap = new HashMap<>();
            resultDto.setErrorCode(ErrorCodes.ERROR_LOGIN_WRONG_PASSWORD.getCode());
            dataMap.put("leftInputCount", leftInputCount);
            resultDto.setData(dataMap);
            return resultDto;
        } catch (LockedAccountException lae) {
            logger.warn("对用户[" + username + "]进行登录验证..验证未通过,账户已锁定");

            userLoginInfo.setErrCode(ErrorCodes.ERROR_LOGIN_ACCOUNT_LOCKED.getCode());
            userLoginInfo.setLoginStatus(false);
            userLoginInfoService.updateUserLoginInfo(userLoginInfo);
            return ResultDto.createResult(ErrorCodes.ERROR_LOGIN_ACCOUNT_LOCKED);
        } catch (ExcessiveAttemptsException eae) {
            logger.warn("对用户[" + username + "]进行登录验证..验证未通过,错误次数过多");
            return ResultDto.createResult(ErrorCodes.ERROR_LOGIN_EXCESSIVE_ATTEMPTS);
        } catch (AuthenticationException ae) {
            //通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景
            logger.warn("对用户[" + username + "]进行登录验证..验证未通过,堆栈轨迹如下" + ae.toString());
            return ResultDto.createResult(ErrorCodes.ERROR_LOGIN_USERNAME_PASSWORD_ERR);
        }
        //验证是否登录成功
        TokenDto tokenDto = null;
        if (subject.isAuthenticated()) {
            logger.info("用户[" + username + "]登录认证通过");

            //init user data
            Principal principal = PrincipalUtils.getPrincipal();
            String ipAddress = getIpAddr(request);
            principal.setIpAddr(ipAddress);

            PrincipalLogUtils.addOperationLog(ModuleNames.SYS, ModuleNames.SYS_LOGIN, UserActions.LOGIN,
                    username + "登陆成功");

            //防止多点登陆
            putSessionToCache();
            tokenDto = new TokenDto(principal.getAccountName(),
                    subject.getSession().getId().toString(),
                    subject.getSession().getTimeout());
        } else {
            token.clear();
        }
        ResultDto result = new ResultDto(0, messageSource.getMessage(MessagesCodes.SUCCESSFUL_USER_LOGIN, null));
        if (tokenDto != null) {
            result.setData(tokenDto);
        }
        return result;
    }

    @RequestMapping(value = "/logout", method = RequestMethod.GET)
    public ResultDto logout(HttpServletRequest request) throws Exception {
        ResultDto dto = new ResultDto();
        if (SecurityUtils.getSubject().isAuthenticated()) { //仅退出已登录用户
            String username = (String) SecurityUtils.getSubject().getPrincipal();//getSession().getAttribute("username");
            //及时将用户注销消息入库
            UserLoginInfo userLoginInfo = new UserLoginInfo();
            userLoginInfo.setUserName(username);
            userLoginInfo.setUserCode(username);
            String cliIp = getIpAddr(request);
            userLoginInfo.setLoginIp(cliIp);
            userLoginInfo.setLoginStatus(false);
            userLoginInfo.setErrCode(0);
            userLoginInfo.setLoginTime(new Date());

            userLoginInfoService.updateUserLoginInfo(userLoginInfo);
            PrincipalLogUtils.addOperationLog(ModuleNames.SYS, ModuleNames.SYS_LOGIN, UserActions.LOGOUT,
                    username + "注销");

            //进行用户的退出，给出提示信息
            SecurityUtils.getSubject().logout();
            dto.setData("您已安全退出");
        }
        return dto;
    }

    /**
     * 检查是否存在同一个账户多点登陆(踢出先登陆者)
     */
    private void putSessionToCache() {
        Principal principal = PrincipalUtils.getPrincipal();
        Serializable sessionId = SecurityUtils.getSubject().getSession().getId();
        String username = principal.getAccountName();
        onlineSessionManager.addOnlineSession(username, sessionId);
    }

    private boolean checkVCode(String code) {
        //如果是测试版本，无需输入，直接验证通过
        if (systemSettings.isVcodeForTest()) {
            return true;
        }
        int clientCode = Integer.parseInt(code);
        if (VCodeController.checkVCode(clientCode)) {
            VCodeController.generateNewRandom(); //IMPORTANT!!! refresh server side v code
            return true;
        }
        //IMPORTANT!!! need refresh server side v code
        VCodeController.generateNewRandom();
        return false;
    }

    /**
     * 获取访问者IP
     * 在一般情况下使用Request.getRemoteAddr()即可，但是经过nginx等反向代理软件后，这个方法会失效。
     * 本方法先从Header中获取X-Real-IP，如果不存在再从X-Forwarded-For获得第一个IP(用,分割)，
     * 如果还不存在则调用Request .getRemoteAddr()。
     *
     * @param request
     * @return
     */
    public static String getIpAddr(HttpServletRequest request) throws Exception {
        String ip = request.getHeader("X-Real-IP");
        if (!StringUtils.isBlank(ip) && !UNKNOWN.equalsIgnoreCase(ip)) {
            return ip;
        }
        ip = request.getHeader("X-Forwarded-For");
        if (!StringUtils.isBlank(ip) && !UNKNOWN.equalsIgnoreCase(ip)) {
            // 多次反向代理后会有多个IP值，第一个为真实IP。
            int index = ip.indexOf(',');
            if (index != -1) {
                return ip.substring(0, index);
            } else {
                return ip;
            }
        } else {
            return request.getRemoteAddr();
        }
    }

    private String decPassword(String password){
        String key1 = "DAB019642892EC3231802E13447D3F52";
        String key2 = "14833CB10FAF594B0874F3494AB0946E";
        String key3 = "42099DDB28D27ADFCFD3E961BE6E08D5";
        if (StringUtils.isNotBlank(password)) {
            return Des.getDesTool().strDec(password, securityKeys.getSecurityKey1(), securityKeys.getSecurityKey2(), securityKeys.getSecurityKey3());
        }
        return password;
    }
}